package jp.wug.core;

import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.TypeLiteral;
import jp.wug.WugRequest;
import jp.wug.util.QueryParameters;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Singleton
public class PageFactory {
    private final TemplateEngine templateEngine = new TemplateEngine();

    /* path info -> Template */
    private final Map<String, Template> templates = new HashMap<String, Template>();

    /* model Class -> Template */
    private final Map<Class<?>, Template> typeToTemplate = new HashMap<Class<?>, Template>();

    private final Map<Class<?>, Template> typeToExportedTemplate = new HashMap<Class<?>, Template>();

    private final Collection<ToBeResolved> toBeResolved = new ArrayList<ToBeResolved>();

    public void register(String path, Document document, KeyedObjectModule keyedObjectModule) throws ClassNotFoundException {
        Element documentElement = document.getDocumentElement();
        String rootAttr;
        if (documentElement.hasAttributeNS(TemplateEngine.NS, "root")) {
            rootAttr = documentElement.getAttributeNS(TemplateEngine.NS, "root");
        } else {
            rootAttr = "java.lang.Void";
        }

        String pathAttr = documentElement.getAttributeNS(TemplateEngine.NS, "path");
        Template template = createTemplate(path, document, rootAttr, keyedObjectModule, path, pathAttr);
        if (typeToTemplate.containsKey(template.getType())) {
            throw new RuntimeException(template.getType() + " is already used in another template.");
        }
        typeToTemplate.put(template.getType(), template);
        templates.put(path, template);
    }

    public void registerForExport(String path, Element element, KeyedObjectModule keyedObjectModule) throws ClassNotFoundException {
        String exportAttr = element.getAttributeNS(TemplateEngine.NS, "export");
        Template template = createTemplate(path, element, exportAttr, keyedObjectModule, path, "");
        typeToExportedTemplate.put(template.getType(), template);
    }

    public void init(Injector injector) {
        for (ToBeResolved t : toBeResolved) {
            t.resolve(injector, this);
        }
        toBeResolved.clear();
    }

    static class Builder {
        private final PageFactory pageFactory;
        private final String templatePath;
        private final KeyedObjectModule keyedObjectModule;

        Builder(PageFactory pageFactory, String templatePath, final KeyedObjectModule keyedObjectModule) {
            this.pageFactory = pageFactory;
            this.templatePath = templatePath;
            this.keyedObjectModule = keyedObjectModule;
            pageFactory.resolveLater(new ToBeResolved() {
                public void resolve(Injector injector, PageFactory pageFactory) {
                    keyedObjectModule.init(injector);
                }
            });
        }

        void addKeyedObject(TypeLiteral<?> keyType, TypeLiteral<?> valueType) {
            keyedObjectModule.add(keyType, valueType);
        }

        <V> Provider<KeyedObjectFactory<String, Model>> getKeyedObjectFactoryProvider(TypeLiteral<V> valueType, ModelFactory modelFactory) {
            Provider<KeyedObjectFactory<String, Model>> provider = keyedObjectModule.getFactoryProvider(valueType, modelFactory);
            return provider;
        }

        void registerForExport(Element element) throws ClassNotFoundException {
            pageFactory.registerForExport(templatePath, element, keyedObjectModule);
        }

        Provider<Template> getTemplateForPage(Class<?> type) {
            PageTemplateProvider provider = new PageTemplateProvider(type);
            pageFactory.resolveLater(provider);
            return provider;
        }

        Provider<Template> getTemplateForImport(Class<?> type) {
            ExportedTemplateProvider provider = new ExportedTemplateProvider(type);
            pageFactory.resolveLater(provider);
            return provider;
        }
    }

    private void resolveLater(ToBeResolved t) {
        toBeResolved.add(t);
    }

    private static class PageTemplateProvider implements Provider<Template>, ToBeResolved {
        private final Class<?> type;
        private Template template;

        private PageTemplateProvider(Class<?> type) {
            this.type = type;
        }

        public void resolve(Injector injector, PageFactory pageFactory) {
            template = pageFactory.getByModelType(type);
        }

        public Template get() {
            return template;
        }
    }

    private static class ExportedTemplateProvider implements Provider<Template>, ToBeResolved {
        private final Class<?> type;
        private Template template;

        private ExportedTemplateProvider(Class<?> type) {
            this.type = type;
        }

        public void resolve(Injector injector, PageFactory pageFactory) {
            template = pageFactory.typeToExportedTemplate.get(type);
        }

        public Template get() {
            return template;
        }
    }

    private interface ToBeResolved {
        void resolve(Injector injector, PageFactory pageFactory);
    }

    static class RequestURLParser {
        private final String pathInfoBase;
        private final Pattern pathPattern;
        private final String pathFormat;
        private final Map<Integer, ModelProperty> pathParameters;
        private final Map<String, ModelProperty> queryParameters;

        private RequestURLParser(String pathInfoBase, String pathFormat, Pattern pathPattern, Map<Integer, ModelProperty> pathParameters, Map<String, ModelProperty> queryParameters) {
            this.pathInfoBase = pathInfoBase;
            this.pathFormat = pathFormat;
            this.pathPattern = pathPattern;
            this.pathParameters = pathParameters;
            this.queryParameters = queryParameters;
        }

        boolean doesMatch(String pathInfo) {
            return pathInfoBase.equals(pathInfo) || pathPattern.matcher(pathInfo).matches();
        }

        /* TODO: escape/unescape query parameters */
        void appendURLParameters(Model root, QueryParameters parameters) {
            List<String> args = new ArrayList<String>();
            for (Map.Entry<Integer, ModelProperty> entry : pathParameters.entrySet()) {
                String[] valueStrings = entry.getValue().get(root);
                String value;
                if (valueStrings.length == 0) {
                    value = "_empty";
                } else if (valueStrings.length == 1) {
                    value = valueStrings[0];
                } else {
                    throw new AssertionError();
                }
                args.set(entry.getKey(), value);
            }
            parameters.setPathInfo(String.format(pathFormat, (Object[]) args.toArray()));

            for (Map.Entry<String, ModelProperty> entry : queryParameters.entrySet()) {
                String[] value = entry.getValue().get(root);
                if (value.length == 0) {
                    parameters.append(entry.getKey(), "_empty");
                } else {
                    for (String s : value) {
                        parameters.append(entry.getKey(), s);
                    }
                }
            }
        }

        List<ModelException> fill(Model root, WugRequest request) {
            List<ModelException> list = new ArrayList<ModelException>();
            Matcher matcher = pathPattern.matcher(request.getPathInfo());
            if (!matcher.find()) {
                for (Map.Entry<Integer, ModelProperty> entry : pathParameters.entrySet()) {
                    String[] value = entry.getValue().get(root);
                    try {
                        entry.getValue().set(root, value);
                    } catch (ModelException e) {
                        list.add(e);
                    }
                }
            } else {
                for (Map.Entry<Integer, ModelProperty> entry : pathParameters.entrySet()) {
                    String[] value = {matcher.group(entry.getKey())};
                    try {
                        entry.getValue().set(root, value);
                    } catch (ModelException e) {
                        list.add(e);
                    }
                }
            }

            for (Map.Entry<String, ModelProperty> entry : queryParameters.entrySet()) {
                String[] parameters = request.getQueryParameter(entry.getKey());
                String[] value;
                if (parameters.length == 0) { /* use default value */
                    value = entry.getValue().get(root);
                } else if (Arrays.asList(parameters).contains("_empty")) {
                    value = new String[0];
                } else {
                    value = parameters;
                }

                try {
                    entry.getValue().set(root, value);
                } catch (ModelException e) {
                    list.add(e);
                }
            }
            return list;
        }


        private static String escapeForRegex(String string) {
            StringBuffer buffer = new StringBuffer();
            for (char c : string.toCharArray()) {
                if ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".indexOf(c) < 0) {
                    buffer.append('\\');
                }
                buffer.append(c);

            }
            return buffer.toString();
        }

        private static String escapeForPrintingFormat(String string) {
            return string.replace("%", "%%");
        }

        static RequestURLParser create(ControllerFactory.ControllerBuilder builder, String pathInfoBase, String pathAttr) throws NoSuchMethodException {
            int n = pathAttr.indexOf('?');
            String pathString;
            String queryString;
            if (n >= 0) {
                pathString = pathAttr.substring(0, n);
                queryString = pathAttr.substring(n + 1);
            } else {
                pathString = pathAttr;
                queryString = "";
            }
            Map<Integer, ModelProperty> pathParameters = new HashMap<Integer, ModelProperty>();
            StringBuffer patternBuffer = new StringBuffer(RequestURLParser.escapeForRegex(pathInfoBase));
            StringBuffer formatBuffer = new StringBuffer(RequestURLParser.escapeForPrintingFormat(pathInfoBase));
            Pattern expressionPattern = Pattern.compile("\\$\\{([^\\}]+)\\}");
            Matcher expressionMatcher = expressionPattern.matcher(pathString);
            int offset = 0;
            int group = 1;
            while (expressionMatcher.find()) {
                int s = expressionMatcher.start();
                int e = expressionMatcher.end();
                patternBuffer.append(RequestURLParser.escapeForRegex(pathString.substring(offset, s)));
                Expression<Void, String[]> getter = builder.call(expressionMatcher.group(1)).asStringsGetter();
                Expression<String[], Void> setter = builder.call(expressionMatcher.group(1)).asStringsSetter();
                pathParameters.put(group, new ModelProperty(getter, setter));
                if (pathString.length() > e) {
                    patternBuffer.append("([^");
                    patternBuffer.append(pathString.charAt(e));
                    patternBuffer.append("]*)");
                } else {
                    patternBuffer.append("(.*)");
                }
                formatBuffer.append(RequestURLParser.escapeForPrintingFormat(pathString.substring(offset, s)));
                formatBuffer.append("%");
                formatBuffer.append(group + 1);
                formatBuffer.append("s");
                group++;
                offset = e;
            }

            Map<String, ModelProperty> queryParameters = new HashMap<String, ModelProperty>();
            Pattern queryPattern = Pattern.compile("([^=&]+)=\\$\\{([^\\}]+)\\}");
            Matcher queryMatcher = queryPattern.matcher(queryString);
            while (queryMatcher.find()) {
                Expression<Void, String[]> getter = builder.call(queryMatcher.group(2)).asStringsGetter();
                Expression<String[], Void> setter = builder.call(queryMatcher.group(2)).asStringsSetter();
                queryParameters.put(queryMatcher.group(1), new ModelProperty(getter, setter));
            }
            return new RequestURLParser(pathInfoBase, formatBuffer.toString(), Pattern.compile(patternBuffer.toString()), pathParameters, queryParameters);
        }
    }

    private static class ModelProperty {
        private final Expression<Void, String[]> getter;
        private final Expression<String[], Void> setter;

        private ModelProperty(Expression<Void, String[]> getter, Expression<String[], Void> setter) {
            this.getter = getter;
            this.setter = setter;
        }

        String[] get(Model model) {
            return getter.call(model, null);
        }

        void set(Model model, String[] strings) throws ModelException {
            setter.tryCall(model, strings);
        }
    }

    private Template createTemplate(String path, Node node, String className, KeyedObjectModule keyedObjectModule, String requestPath, String requestPathAttr) throws ClassNotFoundException {
        Class<?> modelClazz = Class.forName(className);
        TypeLiteral<?> type = TypeLiteral.get(modelClazz);
        ModelFactory modelFactory = new SimpleModelFactory(type);
        ControllerFactory controllerFactory = new ControllerFactory(modelFactory);
        ControllerFactory.ControllerBuilder controllerBuilder = controllerFactory.createPathBuilder(new Builder(this, path, keyedObjectModule));
        Writable writable = templateEngine.process(node.getChildNodes(), controllerBuilder);
        Template template;
        try {
            template = new Template(modelFactory, writable, controllerFactory, controllerBuilder, requestPath, requestPathAttr);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(node + ": ", e);
        }
        toBeResolved.add(template);
        return template;
    }

    private Template getByPath(String pathInfo) {
        Template template = templates.get(pathInfo);
        if (template != null && template.doesMatch(pathInfo)) {
            return template;
        }
        if (!pathInfo.endsWith("/")) {
            return getByPath(pathInfo + "/");
        }
        String key = pathInfo;
        while (key.length() > 0) {
            String k = key.substring(0, key.length() - 1);
            int n = k.lastIndexOf('/');
            key = n == -1 ? "" : key.substring(0, n + 1);
            Template t = templates.get(key);
            if (t != null && t.doesMatch(pathInfo)) {
                return t;
            }
        }
        return null;
    }

    public Class<?> getTypeByPath(String pathInfo) {
        Template template = getByPath(pathInfo);
        if (template == null) {
            return null;
        }
        return template.getType();
    }

    private Template getByModelType(Class<?> type) {
        return typeToTemplate.get(type);
    }

    public Page create(Class<?> type, Object model) {
        return getByModelType(type).create(model);
    }

    ControllerFactory getExportedControllerFactory(Class<?> type) {
        return typeToExportedTemplate.get(type).controllerFactory;
    }

    static class Template implements ToBeResolved {
        private final ModelFactory modelFactory;
        private final Writable writable;
        private final ControllerFactory controllerFactory;
        private final RequestURLParser requestURLParser;
        private Provider<WugRequest> wugRequestProvider;

        Template(ModelFactory modelFactory, Writable writable, ControllerFactory controllerFactory, ControllerFactory.ControllerBuilder controllerBuilder, String requestPath, String requestPathAttr) throws NoSuchMethodException {
            this.modelFactory = modelFactory;
            this.writable = writable;
            this.controllerFactory = controllerFactory;
            requestURLParser = RequestURLParser.create(controllerBuilder, requestPath, requestPathAttr);
        }

        public void resolve(Injector injector, PageFactory pageFactory) {
            controllerFactory.init(pageFactory);
            wugRequestProvider = injector.getProvider(WugRequest.class);
        }

        Class<?> getType() {
            return modelFactory.getType().getRawType();
        }

        Page create(Object object) {
            return new Page(modelFactory.create(object), writable, requestURLParser, controllerFactory.create(), wugRequestProvider);
        }

        boolean doesMatch(String pathInfo) {
            return requestURLParser.doesMatch(pathInfo);
        }

//        public void writeDebugInformation(PrintWriter writer)
//                throws IOException {
//            Writable writable = modelFactory.dumpMeta();
//            WritableContext context = new WritableContext(null, null, null,
//                    null);
//            writable.write(writer, context);
//
//            writer.println();
//
//            // controllerFactory.writeDebugInformation(writer);
//        }
    }
}